Modern architecture ==================== The modern branch of RISE is a ground-up redesign whose distinctives are *architectural*, not cosmetic. The chapter is in two parts: * **Part A** -- what is the same for every model shape (one class, one state space, one set of engines, one validation style). * **Part B** -- what each shape contributes on top: the inputs the factory needs and the behaviours that are intrinsic to that shape. If you only have time for one part, read **Part A** -- it is what makes the toolbox a toolbox rather than a collection of look-alike libraries. **Part B** is the per-shape contract you sign at construction time and is the only place model-type-specific code lives. .. contents:: :local: :depth: 2 Part A. Common across all shapes --------------------------------- One model class ~~~~~~~~~~~~~~~~ The legacy toolbox shipped multiple user-facing classes for the same underlying mathematics -- ``@dsge``, ``@rise``, ``@rfvar``, ``@svar``, ``@prfvar``, ``@proxy_svar``, ``@bvar_dsge``, plus shared backbones in ``@generic`` and ``@estimable``. Each class carried its own methods, sometimes with subtle behavior drift between siblings. The modern toolbox has **one** model class. A DSGE, a reduced-form VAR, a structural VAR with sign restrictions, a proxy SVAR, a DSGE-VAR, and a panel VAR are all instances of the same class with different *shape data* attached. The engines that solve, estimate, filter, and forecast them have a single body each -- there is no ``switch`` on class name in the internals. What this buys you: * A single, predictable API surface. * Cross-shape composition without translator code (a DSGE prior on a VAR is two field assignments, not a sibling class). * The same maintenance fix lands once and benefits every shape. What this costs: * The model object is more polymorphic, so understanding its fields takes one more page of reading on day one. Unified state-space ~~~~~~~~~~~~~~~~~~~~ Every engine sees the model through a single state-space representation. There is no separate code path for constant-parameter linear DSGE, regime-switching DSGE, reduced-form VAR, and so on -- the state space *is* the canonical description, and the engines have one body each. Always regime-switching ~~~~~~~~~~~~~~~~~~~~~~~~ A consequence of the unified state-space: every model is treated as Markov-switching from the start. ``h = 1`` is the trivial non-switching case; ``h > 1`` is a switching model. There is no ``if non_switching`` branch in the solver, the filter, or the estimator. This eliminates a whole class of bugs where the constant-parameter and switching paths drifted apart. No construction metadata after build ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The legacy toolbox stashed user-supplied, class-specific metadata inside the model object after construction -- flags, hints, and overrides that the engines later branched on. The modern toolbox does not. Once a model is parsed and built, the canonical fields are all the engines look at; there is no "construction-time hint" carried through to runtime. Parsers populate canonical names; engines read canonical names. A model object is a *value* describing a state-space, not a record of the path the user took to reach it. Estimate dispatches on properties, not class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In the legacy toolbox, ``estimate`` knew which class of model it was working with and branched accordingly. In the modern toolbox, the estimator dispatches on **properties of the data and the model**, not on the class: * If the data has missing observations or unobserved states, the estimator runs a Kalman filter (or the regime-conditional Kalman filter, if ``h > 1``). * If the data are complete and the model admits a closed-form posterior moment (notably a DSGE-VAR with a conjugate prior), the estimator short-circuits to the closed form. * For nonlinear models the estimator runs the appropriate nonlinear filter. The class is never inspected. User code that constructs a model from a new shape gets correct estimation behaviour *for free*, without touching estimator internals. OOP only at the user boundary ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Object-oriented MATLAB is used where it pays for itself: the user constructs a model with the appropriate factory, calls methods on it, and gets a typed return. Inside the engines, the code is plain MATLAB -- structs, arrays, function handles. The boundary between OO and procedural is the user-facing method. This means the engines are easy to read, easy to step through with the MATLAB debugger, and easy to vectorise. It also means the performance-sensitive code paths are not paying the OOP dispatch tax on every inner-loop call. Arguments blocks systematically ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Every entry point -- user-facing methods, internal helpers, package functions -- declares an ``arguments ... end`` block. The blocks specify default values, sizes, classes, and validators in one place. Hand-rolled ``nargin`` checks, ``inputParser`` boilerplate, and ad-hoc default assignment are absent. Concretely:: function db = irf(m, opts) arguments m (1,1) rise_model opts.irf_periods (1,1) double {mustBePositive} = 40 opts.irf_shock_list (1,:) string = [] opts.irf_regime_specific (1,1) logical = true end ... The block is the contract. The function body assumes its inputs are valid. Errors are raised at the boundary, in MATLAB's standard validator messages, not deep inside the implementation. Stay close to plain MATLAB ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Wherever a modern MATLAB built-in does the job, the modern branch uses the built-in. ``dictionary`` and ``configureDictionary`` replace ``containers.Map``. ``datetime`` and ``calmonths`` replace custom date arithmetic. ``isMATLABReleaseOlderThan`` replaces ``verLessThan``. Old custom utilities -- ``fix_point_*`` option names, custom date types, hand-rolled validators -- are being retired. No internal switching syntax ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In the legacy toolbox, certain solve-time choices were exposed via ``@``-style markers in the model file -- e.g. an ``@commitment`` parameter declared alongside the other parameters but meaning something special to the solver. In the modern toolbox these solve-time choices are options on the ``solve`` call, not parameters in the model file. The model file describes the model; the solve call describes how to solve it. Concretely, commitment vs discretion is selected via:: m = solve(m, solve_policy_type = "discretion"); There is no ``@commitment`` parameter to declare, calibrate, or forget to set. The common engine surface ~~~~~~~~~~~~~~~~~~~~~~~~~~ Once the model is built, every shape sees the same surface of methods. The list below is exhaustive for the common engines -- anything *not* on it is shape-specific (Part B). .. list-table:: :header-rows: 1 :widths: 22 78 * - Engine - Modern method(s) * - Build - ``set``, ``get``, ``solve`` * - Steady state / BGP - ``solve`` (with ``sstate_file``), ``print_steady_state`` * - Filtering - ``filter`` (Kalman / regime-conditional / nonlinear, picked by data + model properties) * - Estimation - ``estimate``, ``pull_objective``, ``log_prior_density``, ``log_posterior_kernel`` * - Posterior simulation - ``rsamplers.rwmh``, ``rsamplers.imh``, ``rsamplers.apt``, ``rsamplers.slice``, ``rsamplers.usrsmplr`` + ``sample`` * - Diagnostics - ``mcmc`` (constructed from draws), ``mdd`` * - Forecast / IRF / decomposition - ``forecast``, ``irf``, ``variance_decomposition``, ``historical_decomposition``, ``simulate`` * - Reporting / plotting - ``rnotes`` (and the ``rprt`` thin subclass), ``fanchart`` / ``plot_fanchart``, ``quick_irfs``, ``quick_plots``, ``plot_probabilities``, ``plot_decomp`` Part B. What each shape contributes ------------------------------------ Each shape is constructed by its own factory. The factory is the **only** place where shape-specific knowledge enters the system. Everything after construction -- the entries in the table above -- is the same across shapes. DSGE -- ``dsge_model`` ~~~~~~~~~~~~~~~~~~~~~~~ Constructed from a model file (``*.rs`` / ``*.rsa`` / ``*.dsge``):: m = dsge_model('nk.rs'); Specific to this shape: * **Model file language** (:doc:`../ModelShapes/DSGE/Model file language`) -- ``@endogenous``, ``@exogenous``, ``@parameters``, ``@model``; Markov-switching parameters via ``@parameters(chain, N) name`` and the implicit ``chain_tp_i_j`` transition probabilities; ``@endogenous(log)`` for log-level variables. * **Steady state / BGP** (:doc:`../ModelShapes/DSGE/Steady state and balanced-growth path`) -- the recommended pattern is a separate ``sstate_file`` (regime-agnostic, returns ``newp.`` for parameters implied by SS conditions). The ``@steady_state_model`` block inside the model file is also supported. * **Optimal policy** (:doc:`../ModelShapes/DSGE/Optimal policy`) -- declared in the model file via ``@optimization_problem`` (single or multi-player); commitment vs. discretion vs. loose commitment vs. stochastic replanning is selected at solve time, not in the model file. * **Optimized simple rules** (:doc:`../ModelShapes/DSGE/Optimized simple rules`) -- the OSR objective is always a runtime argument to ``optimal_simple_rule``; it is not declared in the model file. * **Perturbation order** -- ``solve_order = 1..5``; the engines share one code path across orders. * **Occasionally-binding constraints**, **time-varying transition probabilities** and **deterministic / quasi-deterministic solutions** all live here as well; see the matching chapters under the DSGE shape. Reduced-form VAR -- ``rfvar_model`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: mdl = rfvar_model(endog, ... lag_length = 4, ... constant_term = true, ... deterministic_vars = {}); Specific to this shape: * **Lag length and deterministics** -- ``lag_length``, ``constant_term``, ``deterministic_vars``. * **Priors** -- VAR-coefficient priors are built by the ``var_priors.*`` factories: ``var_priors.minnesota(...)``, ``var_priors.normal_wishart(...)``, ``var_priors.sims_zha(...)``, ``var_priors.independent_normal_wishart(...)``, ``var_priors.diffuse(...)``. They are passed through ``estim_var_priors``. * **Linear restrictions on the coefficients** -- ``estim_linear_restrictions`` accepts strings on the expanded names ``b(, )`` and ``c(, )``. * **Estimation short-circuit** -- with no holes in the data and an uninformative prior, the closed-form OLS / Bayesian posterior is used. With a Minnesota prior on complete data, the Sims-Zha dummy-observation closed form is used. See :doc:`../ModelShapes/Main Reduced form VAR Modeling`. Structural VAR -- ``svar_model`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: mdl = svar_model(endog, ... lag_length = 4, ... constant_term = true); Specific to this shape: * **Identification at the model level** -- a structural VAR is identified *at construction* by zero / linear / sign / nonlinear restrictions on the structural-form coefficients (``a0`` / ``a1`` / ...), wired through the ``estim_*`` options. * **Structural shocks** -- ``structural_shocks``, ``print_structural_form``. * The implication: there is no separate "identify a reduced-form VAR after the fact" step. Reduced-form VARs that need identification at IRF time still use the ``identification`` call from :doc:`../ModelShapes/Main Reduced form VAR Modeling`; an SVAR has already paid that cost at the model level. See :doc:`../ModelShapes/Main Structural VAR Modeling`. Proxy SVAR -- ``proxy_svar_model`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: prox = struct(); prox.var = 'R'; prox.eqtn = 1; prox.coef = 'beta_mp'; prox.shock_eqtn = 'R'; mdl = proxy_svar_model(prox, endog, ... lag_length = 4, ... constant_term = true); Specific to this shape: * **Proxies struct** -- one entry per instrument, with fields ``var`` (variable whose shock is instrumented), ``eqtn`` (the proxy equation number), ``coef`` (relevance parameter :math:`\beta_m`), ``shock_eqtn`` (the structural-shock equation). * **Relevance + noise parameters** (:math:`\beta_m`, :math:`\sigma_m`) are estimated alongside the VAR coefficients and can be made regime-dependent. See :doc:`../ModelShapes/Main Proxy SVAR Modeling`. Panel VAR -- ``prfvar_model`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: panel = struct(); panel.members = {'US','CA','MX','BR'}; panel.homogeneity = 'dynamic'; mdl = prfvar_model(panel, endog, ... lag_length = 4, ... constant_term = true); Specific to this shape: * **Panel struct** -- ``panel.members`` (cross-sectional units), ``panel.homogeneity`` ('pooled', 'meanGroup', 'static', 'dynamic', 'independent'). * **Name expansion** -- internally RISE expands the endogenous list with per-unit suffixes (``GROWTH_US``, ``GROWTH_CA``, ...); restrictions are written on the expanded names. * **Translation back to units** -- ``translate_panel_output`` un-stacks the engine output by unit. See :doc:`../ModelShapes/Main Panel VAR Modeling`. DSGE-VAR -- ``dsge_var_model`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: mdl = dsge_var_model(dsge_model_obj, ... lag_length = 4, ... lambda = 1.0); Specific to this shape: * **DSGE backbone** -- the prior on the reduced-form VAR is the DSGE-implied prior; the DSGE-shaped ``rise_model`` is the first argument to the factory. * **Lambda hyperparameter** -- weighting between DSGE prior and data; ``lambda = inf`` recovers the pure DSGE, ``lambda = 0`` the pure unrestricted VAR. * **No-holes assumption** -- the closed-form posterior moments require complete sample moments; ``dsge_var`` is the one shape for which the estimator requires no missing observations. See :doc:`../ModelShapes/Main DSGE VAR Modeling`. Summary ------- Part A is the bargain: one class, one state space, one set of engines, one validation style, one set of solve-time options. Part B is the *only* place where the shape of your model matters -- the factory you call and the options it accepts. Everything that used to require knowing which sibling class you were working with now requires knowing only which *shape* of model you are working with -- and the engines handle the rest.